package il.technion.cs236369.proxy;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.StringTokenizer;

import org.apache.http.HeaderIterator;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.entity.BufferedHttpEntity;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine;
import org.apache.log4j.Logger;

/**
 * <p>
 *  Deals with all operations to proxy cache
 * </p>
 * 
 * @author Yantsen Maria, 310182316, syantsen@t2
 */
public class Cache {

	private static Connection conn;
	private static String tableName;
	private static Boolean cacheActive = Boolean.FALSE;

	/**
	 * Creates a connection to data base.
	 *
	 * @param dbDriver the data base driver
	 * @param dbURL the data base url
	 * @param dbUsername the data base username
	 * @param dbPassword the data base password
	 * @param dbName the data base name
	 * @param tblName the table name name
	 */
	public static void connect(String dbDriver, String dbURL, String dbUsername, String dbPassword, String dbName,
			String tblName) {
		try {
			// save table name
			tableName = tblName;

			// get driver
			Class.forName(dbDriver.trim());

			// Create and open connection
			conn = DriverManager.getConnection(dbURL + dbName, dbUsername, dbPassword);

			// DEBUG code
			Logger.getLogger(LogUtils.cache).info("Cache connection installed\n");

			cacheActive = Boolean.TRUE;
			// DEBUG
			// clean();
			// outputDb();

		} catch (SQLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage(), e);
			System.err.println("Cache: connection with cache did not established.");
			cacheActive = Boolean.FALSE;
		} catch (ClassNotFoundException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage(), e);
			System.err.println("Cache: sql driver not found;");
			cacheActive = Boolean.FALSE;
		} finally {
			if (cacheActive == Boolean.FALSE && conn != null) {
				try {
					conn.close();
				} catch (SQLException e) {
					System.err.println("Cache: close connection failure.");
				}
			}
		}

	}

	/**
	 * Finds the entry in the cache according to the URL and returns it as the reply.
	 *
	 * @param request the request
	 * @return reply build according to data stored in the cache or null if there is no appropriate entry
	 */
	public static BasicHttpResponse get(final HttpRequest request) {

		if (!cacheActive.booleanValue()) {
			return null;
		}

		BasicHttpResponse response = new BasicHttpResponse(new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK,
				"OK"));
		PreparedStatement stmt = null;
		ResultSet rs = null;
		try {

			//Logger.getLogger(LogUtils.cache).info("#### URI " + request.getRequestLine().getUri());
			// String url = (new URI(request.getRequestLine().getUri())).toURL().toString();

			String url = URLEncoder.encode(request.getRequestLine().getUri(), "UTF8");

			if (url.getBytes().length > 255) {
				Logger.getLogger(LogUtils.cache).info("URL too Big for get\n");
				return null;
			}

			stmt = conn.prepareStatement("SELECT * FROM " + tableName + " WHERE url = ?;");
			stmt.setString(1, url);
			stmt.execute();
			rs = stmt.getResultSet();

			if (!rs.next()) {
				Logger.getLogger(LogUtils.cache).info("The entry for the url " + url + " not found\n");
				return null;
			}

			rs.last();
			if (rs.getRow() != 1) {
				Logger.getLogger(LogUtils.cache).error("More than 2 records for this url\n");
				return null;
			}

			StringTokenizer headersTokenizer = new StringTokenizer(rs.getString(2), "\r\n");
			while (headersTokenizer.hasMoreTokens()) {
				String fullHeader = headersTokenizer.nextToken();
				StringTokenizer keyValTokenizer = new StringTokenizer(fullHeader, ":");

				String headerName = keyValTokenizer.nextToken();
				String headerValue = fullHeader.substring(headerName.length() + 2);

				// response.addHeader(keyValTokenizer.nextToken(), keyValTokenizer.nextToken().substring(1));
				response.addHeader(headerName, headerValue);
			}

			Blob b = rs.getBlob(3);
			BasicHttpEntity entity = new BasicHttpEntity();
			entity.setContent(b.getBinaryStream());
			response.setEntity(new BufferedHttpEntity(entity));

		} catch (MalformedURLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: Uri parsing failed");
			response = null;
		} catch (SQLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: error in sql query");
			response = null;
		} catch (IOException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: error during IO operation");
			response = null;
		} finally {
			if (stmt != null)
				try {
					stmt.close();
					if (rs != null)
						rs.close();
				} catch (SQLException e) {
				}

		}
		return response;
	}

	/**
	 * Stores response in the cache, according to URL in the previous request.
	 *
	 * @param request the request from client
	 * @param response the response for the request
	 */
	public static void store(final HttpRequest request, final HttpResponse response) {

		if (!cacheActive.booleanValue()) {
			return;
		}

		try {

			if (contains(new URI(request.getRequestLine().getUri()))) {
				// if we already have the entrance with url in cache, remove and enter new one
				Logger.getLogger(LogUtils.cache).info("The reply was already cached\n");
				deleteEntry(new URI(request.getRequestLine().getUri()));
			}

			String url = (new URI(request.getRequestLine().getUri())).toURL().toString();
			if (url.getBytes().length > 255) {
				Logger.getLogger(LogUtils.cache).info("Url is to Big for store\n");
				return;
			}

			String headers = "";
			HeaderIterator it = response.headerIterator();
			while (it.hasNext()) {
				headers += it.next();
				headers += "\r\n";
			}

			PreparedStatement insertDataStmt = conn.prepareStatement("INSERT INTO " + tableName + " VALUES (?,?,?);");
			insertDataStmt.setString(1, url);
			insertDataStmt.setString(2, headers);

			// create repeatable entry
			BufferedHttpEntity buffEntity = new BufferedHttpEntity(response.getEntity());

			if (response.getEntity().getContent() != null) {
				// add entry to statement
				insertDataStmt.setBlob(3, buffEntity.getContent());
			}
			// restore the entry in the original request
			response.setEntity(buffEntity);

			Logger.getLogger(LogUtils.cache).info("Storing reply in cache\n");

			// cache the data
			insertDataStmt.executeUpdate();

			// DEBUG code
			// outputDb();

		} catch (URISyntaxException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: Uri parsing failed");
		} catch (IOException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: error during IO operation");
		} catch (SQLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: error in sql query");
		}

	}

	/**
	 * Delete entry in cache according to URI.
	 *
	 * @param uri the uri
	 */
	public static void deleteEntry(URI uri) {

		if (!cacheActive.booleanValue()) {
			return;
		}

		PreparedStatement stmt;
		try {
			Logger.getLogger(LogUtils.cache).info("Delete entry from cache\n");
			stmt = conn.prepareStatement("DELETE FROM " + tableName + " WHERE url = ?;");
			stmt.setString(1, uri.toURL().toString());
			stmt.execute();

		} catch (SQLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: error in sql query");
		} catch (MalformedURLException e) {
			Logger.getLogger(LogUtils.cache).error(e.getMessage() + "\n", e);
			System.err.println("Cache: Url reading failed");
		}
	}

	/**
	 * Check if the cache contains entry with given uri.
	 *
	 * @param uri the uri
	 * @return true, if cache contains entry with given uri
	 * @throws SQLException 
	 * @throws MalformedURLException
	 */
	private static boolean contains(URI uri) throws SQLException, MalformedURLException {

		String url;

		url = uri.toURL().toString();
		if (url.getBytes().length > 255)
			return false;

		PreparedStatement stmt = conn.prepareStatement("SELECT * FROM " + tableName + " WHERE url = ?;");
		stmt.setString(1, url);
		stmt.execute();
		ResultSet rs = stmt.getResultSet();

		if (rs.next()) {
			return true;
		}

		return false;
	}

	/**
	 * Close connection.
	 *
	 * @throws SQLException
	 */
	public static void closeConnection() throws SQLException {
		conn.close();
	}

	/**
	 * Prints all data base entries to log file.
	 *
	 * @throws SQLException
	 */
	public static void outputDb() throws SQLException {
		String res = "\n<<<\tC A C H E\tC O N T E N T\t>>>\n";
		Statement stmt = conn.createStatement();
		ResultSet rs = stmt.executeQuery("SELECT * FROM " + tableName + ";");
		while (rs.next()) {
			res += ("\n=======\tentry\t======");
			res += ("\nURL: " + rs.getString(1));
			res += ("\nHeaders: " + rs.getString(2));
			// res += ("\nEntity: " + rs.getString(3));
			res += ("\n======================");
		}
		res += ("\n<<<------------------------------------->>>\n");
		if (stmt != null)
			stmt.close();
		if (rs != null)
			rs.close();
		Logger.getLogger(LogUtils.cache).info(res);
	}

}
